#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <memory>

template<class T>
class Buffer
{
private:
    mutable std::mutex mut;
    std::queue<T> data_queue;
    std::condition_variable data_con;
    int max_size_;

public:
    Buffer(int max_size = 0){
        max_size_ = max_size;
    }
    Buffer(Buffer const& other, int max_size = 0)
    {
        max_size_ = max_size;
        std::lock_guard<std::mutex> lk(other.mut);
        data_queue = other.data_queue;
        while((max_size_ != 0) && (data_queue.size() > max_size_))
            data_queue.pop();
    }
    void push(T tValue)
    {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(tValue);
        if((max_size_ != 0) && (data_queue.size() > max_size_))
            data_queue.pop();
        data_con.notify_one();
    }

    void wait_and_pop(T& tValue)
    {
        std::unique_lock<std::mutex> lk(mut);
        data_con.wait(lk,[this]{return !data_queue.empty();});
        tValue = data_queue.front();
        data_queue.pop();
    }

    bool wait_and_pop_for_ms(T& tValue, int microseconds = 10)
    {
        std::unique_lock<std::mutex> lk(mut);
        if(data_con.wait_for(lk, std::chrono::microseconds(microseconds),
                             [this]{return !data_queue.empty();})){
            tValue = data_queue.front();
            data_queue.pop();
            return true;
        }
        else{
            tValue = T();
            return false;
        }
    }

    std::shared_ptr<T>wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        data_con.wait(lk,[this]{return !data_queue.empty();});
        std::shared_ptr<T> ret (std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return ret;
    }

    bool try_pop(T& tValue)
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
            return false;
        tValue = data_queue.front();
        data_queue.pop();
        return true;
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
            return std::shared_ptr<T>();
        std::shared_ptr<T> ret(std::make_shared(data_queue.front()));
        data_queue.pop();
        return ret;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

class Thread{
public:
    Thread();
    virtual ~Thread(){}
    virtual void main_loop_impl() = 0;
    bool main_loop();
    bool start();
    bool stop();
    bool set_buffer(Buffer<int>* _buffer);
    bool set_period(int _period);

protected:
    Buffer<int>* buffer_;
    int period_;

private:
    bool stop_;
    std::thread* thread_;
};

Thread::Thread(){
    stop_ = false;
    buffer_ = nullptr;
    thread_ = nullptr;
    period_ = 100;
}

bool Thread::set_period(int _period){
    period_ = _period;
    return true;
}

bool Thread::start(){
    if(thread_ == nullptr){
        thread_ = new std::thread(std::mem_fn(&Thread::main_loop), this);
        thread_->detach();
    }
    return true;
}

bool Thread::main_loop(){
    while(!stop_){
        main_loop_impl();
        std::this_thread::sleep_for(std::chrono::milliseconds(period_));
    }
    return true;
}

bool Thread::stop(){
    stop_ = true;
    return true;
}

bool Thread::set_buffer(Buffer<int> *_buffer){
    buffer_ = _buffer;
    return true;
}

class Productor : public Thread{
public:
    void main_loop_impl();
};

void Productor::main_loop_impl(){
    static int counter = 0;
    buffer_->push(counter++);
}

class Consumer : public Thread{
public:
    void main_loop_impl();
};

void Consumer::main_loop_impl(){
    int value;
    if(buffer_->try_pop(value))
        std::cout << "get value ::  " << value << std::endl;
    else
        std:: cout << "no value" << std::endl;
}

int main(){
    Buffer<int> buffer;
    Productor productor;
    Consumer consumer_1;
    Consumer consumer_2;

    productor.set_period(50);
    consumer_1.set_period(80);
    consumer_2.set_period(70);

    productor.set_buffer(&buffer);
    consumer_1.set_buffer(&buffer);
    consumer_2.set_buffer(&buffer);

    productor.start();
    consumer_1.start();
    consumer_2.start();

    for(int i = 1; i < 10; i++){
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }

    productor.stop();
    consumer_1.stop();


    for(int i = 1; i < 10; i++){
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }

    return 0;
}
